package org.haptimap.hcimodules.scanning;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.haptimap.hcimodules.util.OrientationModule;
import org.haptimap.hcimodules.util.WayPoint;

import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.os.Handler;
import android.os.Vibrator;
import android.speech.tts.TextToSpeech;
import android.util.Log;

/**
 * <p>The class <code>ScanOrientation</code> contains methods for performing basic
 * scanning when a list of {@link WayPoint}s is added.</p>
 * 
 * <p>This module filters points of interest on a distance basis. The idea is to group 
 * points of interest with a criterion of minimum and maximum distance in meter from the 
 * user's location. Then the module picks up the point that is closest to 0 degrees relative 
 * to where the user is pointing with his/her device.</p>
 *
 * <p>Then scanning angle used in this module is +(-)15 degrees and the points of interest 
 * located within these values will be candidates for feedback.</p> 
 * 
 * <p>The criterion can be changed with {@link #setCustomMinDistanceValue(int)} and 
 * {@link #setCustomMaxDistanceValue(int)} The default values are {@value #MAX_DISTANCE_VALUE} 
 * meters for the maximum and {@value #MIN_DISTANCE_VALUE} meters for the minimum. These values 
 * are the default recommendations and can be restored by calling the {@link #setDefaultDistanceValues()}.</p>
 * 
 * <p>This module offers the use of a listener, {@link OnScanOrientationEventListener}, which will be
 * called on different events. The listener is registered using {@link #setOnScanOrientationEventListener(OnScanOrientationEventListener)}
 * and unregistered using {@link #unregisterScanOrientationEventListener()}.
 * 
 * <p>The module follows a state machine, which can be compared to the regular Android approach. The
 * states that included are: {@link #onStart()} -> {@link #onResume()} -> {@link #onPause()} -> {@link #onStop()} 
 * and {@link #onDestroy()}
 * 
 * <p>The interaction will not start until the device gets a GPS fix and it has been initialized using
 * {@link #onStart()}. Once the interaction starts and a point of interest if found, 
 * the {@link OnScanOrientationEventListener#onTargetFound(String, int, float)} will be fired and data 
 * about the point is received.</p> 
 * 
 * <p>The available constructors in this class are:
 * <blockquote>
 * <pre>
 * ScanOrientation(Context context)
 * ScanOrientation(Context context, List<WayPoint> waypoints)
 * </pre>
 * </blockquote>
 *   
 * <p>This module offers distance feedback encoded as pauses between vibrations. 
 * The longer the pause, the larger the distance to the point of interest at which the user is pointing.
 * However, it is possible to turn off the distance encoding by calling the {@link #setDistanceFeedbackEnabled(boolean)}</p>
 * 
 * <p>Besides vibration feedback, this module does offer speech as an alternative and it is also possible
 * to use both modes. The feedback is available using {@link #setHapticFeedbackEnabled(boolean)} and
 * {@link #setSpeechFeedbackEnabled(boolean)}</p>
 * 
 * <p>This module <strong>must</strong> have the following permissions in the AndroidManifest.xml file:
 * <ul>
 * <li>{@link permission#VIBRATE}</li> 
 * <li>{@link permission#ACCESS_FINE_LOCATION}</li>
 * </ul>
 * </p>
 * 
 * @see OnScanOrientationEventListener
 * @see HapticScanOrientation
 * @see SpeechScanOrientation
 * @see WayPoint
 * @see HCIModule 
 * 
 * @author Miguel Molina, August 2011
 */
public class ScanOrientation extends OrientationModule{

	private static final String TAG = "ScanOrientation";
	private static final boolean DEBUG = false;

	/**
	 * Default value for the max deviation from target.
	 */
	private static final int MAX_DEVIATION = 15;

	/**
	 * Recommended default values for a distance range where the 
	 * points of interest might be found. 
	 */
	private static final int MAX_DISTANCE_VALUE = 300;
	private static final int MIN_DISTANCE_VALUE = 0;

	//Flags representing the different modes for this module.
	private static final int DISTANCE_FEEDBACK = 0x00000001;
	private static final int SPEECH_FEEDBACK = 0x00000002;
	private static final int HAPTIC_FEEDBACK = 0x00000004;

	//Default rates/postdelay values according to kind of interaction.
	private static final int DEFAULT_RATE_INTERVAL = 1000;
	private static final int DEFAULT_SPEECH_RATE_INTERVAL = 2300;

	//Vibrator values. 
	private Vibrator mVibrator;
	private static final int NO_REPEAT = -1;

	//Default pattern used for feedback when a point of interest is found 
	private long[] bearingPattern = {0, 100, 100, 200};

	//Text To Speech values.
	private TextToSpeech tts;
	private boolean ttsIsReady;

	//The value containing flags.
	private int mCurrentFlags;

	//The current values for the distance range used in the interaction
	private int mCurrentMaxDistance;
	private int mCurrentMinDistance;

	private WPoint mPrevWPoint;

	//Two lists: list containing the original values and sorted by distance, and 
	//sortedList contains an ordered (by deviation) list with valid point of interest.
	private List<WPoint> mList;	
	private List<WPoint> mSortedList;

	//The last known scanning type
	private boolean feedbackTypeChanged;
	private FeedbackType prevFeedbackType;

	//The listener
	private OnScanOrientationEventListener listener;

	//Comparator used to sort lists
	private Comparator<? super WPoint> mDeviationComparator;
	private Comparator<? super WPoint> mDistanceComparator;

	//The Handler that is in charge support the Runnable. 
	private Handler mHandler;
	private boolean running;
	private float mCurrentPeriod = Float.NaN;

	/**
	 * Default constructor that initializes this module.
	 * 
	 * @param context The context expected is an {@link Activity#getApplicationContext()}
	 */
	public ScanOrientation(Context context) {
		super(context);
		initValues();
		if(DEBUG) Log.d(TAG, "ScanOrientation created");
	}

	/**
	 * Constructor that initializes this module and adds a list of waypoints.
	 * 
	 * @param context The context expected is an {@link Activity#getApplicationContext()}
	 * @param waypoints List containing waypoints.
	 */
	public ScanOrientation(Context context, List<WayPoint> waypoints){
		this(context);
		this.addWayPoints(waypoints);
	}

	@Override
	public void onStart() {
		super.onStart();
		startHandler();
		if(DEBUG) Log.d(TAG, "onstart");
	}	

	@Override
	public void onPause() {
		super.onPause();
		mHandler.removeCallbacks(update);
		running = false;
	}

	@Override
	public void onResume() {
		super.onResume();
		startHandler();
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		destroyHandler();
		destroyTTS();
		destroyVibrator();
		if(DEBUG) Log.d(TAG, "onDestroy()");
	}

	/**
	 * Initialize values
	 */
	private void initValues(){
		this.mCurrentMaxDistance = MAX_DISTANCE_VALUE;
		this.mCurrentMinDistance = MIN_DISTANCE_VALUE;
		this.feedbackTypeChanged = true;
		this.mList = new ArrayList<ScanOrientation.WPoint>();
		this.mSortedList = new ArrayList<ScanOrientation.WPoint>();
		this.mDeviationComparator = new SortedByDeviation();
		this.mDistanceComparator = new SortedByDistance();
		this.mHandler = new Handler();
		this.mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
		this.tts = new TextToSpeech(context, new TextToSpeech.OnInitListener() {

			public void onInit(int status) {
				ttsIsReady = (status == TextToSpeech.SUCCESS);		
				tts.setPitch(0.8f);
				tts.setSpeechRate(1.0f);
				tts.setLanguage(Locale.getDefault());		
			}
		});		
	}

	/**
	 * Starts the handler and post the Runnable that updates waypoint values
	 */
	private void startHandler(){
		if(!running){
			mHandler.post(update);
			running = true;
		} else {
			Log.w(TAG, "handler already started...");
		}
	}

	//Destroys the current handler and remove all callbacks.
	private void destroyHandler(){
		if(mHandler != null){
			mHandler.removeCallbacks(update);
			running = false;
			mHandler = null;
		}
	}

	//Destroys the TTS
	private void destroyTTS(){
		if(ttsIsReady && tts != null){
			tts.stop();
			tts.shutdown();
			ttsIsReady = false;
			tts = null;
		}
		return;
	}

	//Destroys the vibrator
	private void destroyVibrator(){
		if(mVibrator != null){
			mVibrator.cancel();
			mVibrator = null;
		}
	}

	/**
	 * Add a list of {@link WayPoint} that this module will use when scanning.
	 * The list should contain the points of interest that one would like to interact
	 * with. The list might not be null.
	 * 
	 * The {@link Waypoint} might also been added when creating the class with one 
	 * of the constructors.
	 *  
	 * @param waypoints The list containing {@link WayPoint}s
	 * @throws IllegalArgumentException If the list is null
	 * @throws IllegalArgumentException If the list is empty
	 * @see {@link ScanOrientation#ScanOrientation(Context, List)}
	 */
	public void addWayPoints(List<WayPoint> waypoints){
		if(waypoints == null){
			throw new IllegalArgumentException("waypoints == null");
		}
		if(waypoints.isEmpty()){
			throw new IllegalArgumentException("waypoints is empty");
		}
		for(WayPoint wp : waypoints){
			mList.add(new WPoint(wp));
		}	
	}
	
	/**
	 * When a list of waypoints is not available, single waypoints can be added to this module.
	 * 
	 * @param wayPoint The waypoint that is to be added to the list of available points of interest.
	 * 
	 * @see #addWayPoints(List)
	 * 
	 * @throws IllegalArgumentException If the waypoint is null
	 */
	public void addWayPoint(WayPoint wayPoint){
		if(wayPoint == null){
			throw new IllegalArgumentException("wayPoint == null");
		}
		mList.add(new WPoint(wayPoint));
	}
	
	/**
	 * Some points of interest might not longer be interested for the interaction and can be 
	 * removed using this method.
	 * 
	 * @param wayPoint The {@link WayPoint} that should be removed
	 * 
	 * @return Whether the object was successfully removed.
	 */
	public boolean removeWayPoint(WayPoint wayPoint){
		Iterator<WPoint> itr = mList.iterator();
		boolean found = false;
		WPoint wp;
		while(itr.hasNext() && !found){
			wp = itr.next();
			if(wp.wayPoint.equals(wayPoint)){				
				found = mList.remove(wp);
			}
		}
		return found;
	}

	Runnable update = new Runnable() {

		public void run() {
			updateWPObjects();
			sortWPoints();
			if(!mSortedList.isEmpty()){
				perform();
			}
			mHandler.postDelayed(this, getPostDelayRate());
		}
	};

	/**
	 * Checks if the distance value is within the range given by the {@link #mCurrentMaxDistance}
	 * and the {@link #mCurrentMinDistance}
	 * 
	 * @param distance The value that is about to be checked
	 * @return whether the value is within the given range 
	 */
	private boolean distanceIsValid(float distance){
		return (distance > mCurrentMinDistance && distance < mCurrentMaxDistance);
	}

	/**
	 * Add new points of interest to a list. The list is sorted by deviation finding the 
	 * point of interest that is closest to the user's pointing direction.
	 */
	private void sortWPoints(){
		mSortedList.clear();
		sortByDistance();
		Iterator<WPoint> itr = mList.iterator();
		WPoint wp;
		while(itr.hasNext()){
			wp = itr.next();
			if(distanceIsValid(wp.mDistance)){
				mSortedList.add(wp);
			}
		}
		Collections.sort(mSortedList, mDeviationComparator);
	}

	/**
	 * Performs the action to be taken. The interaction depends whether the {@link #setHapticFeedbackEnabled(boolean)}
	 * or/and the {@link #setSpeechFeedbackEnabled(boolean)} are enabled/disabled.
	 */
	private synchronized void perform(){
		WPoint tmp = mSortedList.get(0);

		if(Math.abs(tmp.mDeviation) <= MAX_DEVIATION){
			if(tmp.equals(mPrevWPoint) && mCurrentPeriod > DEFAULT_RATE_INTERVAL){
				mCurrentPeriod -= DEFAULT_RATE_INTERVAL;
			} else {
				switch(getFeedbackType()){

				case HAPTIC:
					vibrate();
					break;

				case HAPTIC_AND_DISTANCE:
					vibrate();				
					break;

				case SPEECH_WP_NAME:
					talk(tmp.wayPoint.getName());
					break;

				case SPEECH_WP_NAME_AND_DISTANCE:			
					talk(tmp.wayPoint.getName() + ", " + (int)tmp.mDistance);
					break;

				case SPEECH_AND_HAPTIC:
					talk(tmp.wayPoint.getName());
					vibrate();		
					break;

				case SPEECH_AND_HAPTIC_AND_DISTANCE:
					talk(tmp.wayPoint.getName() + ", " + (int)tmp.mDistance);
					vibrate();		
					break;

				case NOT_VALID:
					if (DEBUG) Log.d(TAG, "no valid interaction type... ");
					break;
				}
				this.listener.onTargetFound(tmp.wayPoint.getName(), (int) tmp.mDistance, tmp.mDeviation);
				mCurrentPeriod = calculatePeriod(tmp.mDistance);
			}
		} else {
			mCurrentPeriod = DEFAULT_RATE_INTERVAL;
		}
		mPrevWPoint = tmp;
	}

	//vibrate with no repeat
	private void vibrate(){
		if(mVibrator != null){
			mVibrator.vibrate(bearingPattern, NO_REPEAT);
		}
	}

	/**
	 * The postdelay value depends whether the next interaction is lower than the {@link #DEFAULT_RATE_INTERVAL}
	 * In that case, there is a value that indicates that the distance is a flag and the handler should consider
	 * when to run the Runnable again.
	 * 
	 * @return The current value used by the Handler
	 */
	private synchronized long getPostDelayRate() {
		return (long) ((mCurrentPeriod < DEFAULT_RATE_INTERVAL) ? mCurrentPeriod : DEFAULT_RATE_INTERVAL);
	}

	/**
	 * Calculates the period based on the type of flags that are enabled. The distance value will not 
	 * exceed {@link #MAX_DISTANCE_VALUE}
	 * @param distance The distance from the user's position to the point of interest
	 * @return value based on the flags for the type of feedback. 
	 */
	private synchronized float calculatePeriod(float distance){
		float ans = DEFAULT_RATE_INTERVAL;
		if(isDistanceFeedbackEnabled()){
			if(distance > MAX_DISTANCE_VALUE) distance = MAX_DISTANCE_VALUE;
			ans += Math.round((Math.log(distance))*distance);
		}
		if(isSpeechFeedbackEnabled()) ans +=DEFAULT_SPEECH_RATE_INTERVAL;
		return ans;
	}

	/**
	 * The feedback depends on the kind of interaction that is available through the flags.
	 * When one of the {@link #setDistanceFeedbackEnabled(boolean)} or {@link #setSpeechFeedbackEnabled(boolean)}
	 * or {@link #setHapticFeedbackEnabled(boolean)} change status, this method will check for 
	 * which feedback this module should provide. In other case, the last known one will be used.
	 * 
	 * @return The feedback available for the current interaction
	 */
	private FeedbackType getFeedbackType(){
		FeedbackType ans = FeedbackType.NOT_VALID;
		if(feedbackTypeChanged){
			if (isSpeechFeedbackEnabled() && isHapticFeedbackEnabled()){
				if (isDistanceFeedbackEnabled()){
					ans = FeedbackType.SPEECH_AND_HAPTIC_AND_DISTANCE;
				} else {
					ans = FeedbackType.SPEECH_AND_HAPTIC;
				}
			} else if (isSpeechFeedbackEnabled()) {
				if (isDistanceFeedbackEnabled()) {
					ans = FeedbackType.SPEECH_WP_NAME_AND_DISTANCE; 
				} else {
					ans = FeedbackType.SPEECH_WP_NAME;
				}
			} else if (isHapticFeedbackEnabled()) {
				if (isDistanceFeedbackEnabled()) {
					ans = FeedbackType.HAPTIC_AND_DISTANCE;
				} else {
					ans = FeedbackType.HAPTIC;
				}
			}
			prevFeedbackType = ans;
			feedbackTypeChanged = false;
		} else {
			ans = prevFeedbackType;
		}
		return ans;
	}

	/**
	 * This enum specifies the type of feedback this module is intended to provide.
	 *
	 */
	private enum FeedbackType{
		SPEECH_WP_NAME,
		SPEECH_WP_NAME_AND_DISTANCE,
		HAPTIC,
		HAPTIC_AND_DISTANCE,
		SPEECH_AND_HAPTIC,
		SPEECH_AND_HAPTIC_AND_DISTANCE,
		NOT_VALID
	}

	/**
	 * General method used to speak by flushing the current queue and adding the specified string.
	 * 
	 * @param speak The string that will be spoken
	 */
	private void talk(String speak){
		if(ttsIsReady){
			tts.speak(speak, TextToSpeech.QUEUE_FLUSH, null);
		}
		return;
	}

	//Refresh the current values
	private synchronized void updateWPObjects(){
		for(WPoint wpo : mList){
			wpo.updateValues();
		}
		if(DEBUG) Log.d(TAG, "updating...");
	}

	/**
	 * Set the upper limit in meters for this module. The default value is {@value #MAX_DISTANCE_VALUE} meters.
	 * 
	 * <p> All points of interest that are located below this custom distance will be
	 * filtered and used when scanning for Points of interest.
	 * 
	 * @param distance The max distance that will be valid for scanning
	 * @throws IllegalArgumentException if the distance < 0 
	 * @throws IllegalArgumentException if distance < minimum distance. 
	 * 
	 * @see #setCustomMinDistanceValue(int)
	 * @see #setDefaultDistanceValues()
	 */
	public void setCustomMaxDistanceValue(int distance){
		if(distance < 0){
			throw new IllegalArgumentException("distance < 0");
		}
		if(distance < this.mCurrentMinDistance){
			throw new IllegalArgumentException("distance < min distance value");
		}
		this.mCurrentMaxDistance = distance;
	}

	/**
	 * Set the lower limit in meters for this module. The default value is {@value #MIN_DISTANCE_VALUE} meters.
	 * 
	 * <p> All points of interest that are located above this custom distance will be
	 * filtered and used when scanning for Points of interest.
	 * 
	 * @param distance The min distance that will be valid for scanning
	 * 
	 * @throws 	IllegalArgumentException if the distance > custom max distance value OR {@value #MAX_DISTANCE_VALUE}
	 * 			if the default value has not been changed
	 *  
	 * @throws IllegalArgumentException if distance < 0 
	 * 
	 * @see #setCustomMinDistanceValue(int)
	 * @see #setDefaultDistanceValues()
	 */
	public void setCustomMinDistanceValue(int distance){
		if(distance > this.mCurrentMaxDistance){
			throw new IllegalArgumentException("distance > max distance value");
		}
		if(distance < 0){
			throw new IllegalArgumentException("distance < 0");
		}
		this.mCurrentMinDistance = distance;
	}

	/**
	 * @return The current minimum distance from where the points of interest are valid.
	 */
	public int getCurrentMinDistanceValue(){
		return new Integer(this.mCurrentMinDistance);
	}

	/**
	 * @return The maximum distance value to where the point of interest will be considered.
	 */
	public int getCurrentMaxDistanceValue(){
		return new Integer(this.mCurrentMaxDistance);
	}

	/**
	 * <p>Restores the default range values for maximum and minimum distances.</p>
	 * <ul>
	 * <li>Default minimum = {@value #MIN_DISTANCE_VALUE} meters</li>
	 * <li>Default maximum = {@value #MAX_DISTANCE_VALUE} meters</li>
	 * </ul>
	 */
	public void setDefaultDistanceValues(){
		this.mCurrentMinDistance = MIN_DISTANCE_VALUE;
		this.mCurrentMaxDistance = MAX_DISTANCE_VALUE;
	}

	/**
	 * Sorting points of interest from the mList by distance. 
	 */
	private void sortByDistance(){
		Collections.sort(mList, mDistanceComparator);
	}

	/**
	 * Enables whether the distance should be considered when encoding the feedback.
	 * If the value is enabled, it will affect both the haptic and the speech feedback.
	 * 
	 * @param distanceFeedbackEnabled Whether this feedback should be enabled
	 * 
	 * @see #setHapticFeedbackEnabled(boolean)
	 * @see #setSpeechFeedbackEnabled(boolean)
	 */
	public void setDistanceFeedbackEnabled(boolean distanceFeedbackEnabled){
		if(distanceFeedbackEnabled){
			mCurrentFlags |= DISTANCE_FEEDBACK;
		} else {
			mCurrentFlags &= ~DISTANCE_FEEDBACK;
		}
		feedbackTypeChanged = true;
	}

	/**
	 * @return Whether this feedback value is enabled
	 */
	public boolean isDistanceFeedbackEnabled(){
		return DISTANCE_FEEDBACK == (mCurrentFlags & DISTANCE_FEEDBACK);
	}

	/**
	 * <p>Enables the haptic feedback for this module. If enabled, the module will create 
	 * small vibration patterns used when a point of interest if found. The period between
	 * vibrations might alter whether the distance feedback is enabled.</p>
	 * 
	 * <p>This feedback might be used together with speech feedback</p>
	 *  
	 * @param hapticFeedbackEnabled Whether this feedback should be enabled
	 * 
	 * @see #setDistanceFeedbackEnabled(boolean)
	 * @see #setSpeechFeedbackEnabled(boolean)
	 */
	public void setHapticFeedbackEnabled(boolean hapticFeedbackEnabled){
		if(hapticFeedbackEnabled){
			mCurrentFlags |= HAPTIC_FEEDBACK;
		} else {
			mCurrentFlags &= ~HAPTIC_FEEDBACK;
		}
		feedbackTypeChanged = true;
	}

	/**
	 * @return Whether this feedback is enabled
	 */
	public boolean isHapticFeedbackEnabled(){
		return HAPTIC_FEEDBACK == (mCurrentFlags & HAPTIC_FEEDBACK);
	}

	/**
	 * <p>Enables the speech feedback for this module. If enabled, the module will 
	 * use the {@link TextToSpeech} Engine in order to utter information when 
	 * a point of interest if found. The period between interactions might change whether 
	 * the distance feedback is enabled.</p>
	 * 
	 * <p>This feedback might be used together with the haptic feedback</p>
	 * 
	 * @param speechFeedbackEnabled Whether this feedback should be enabled
	 * 
	 * @see #setDistanceFeedbackEnabled(boolean)
	 * @see #setHapticFeedbackEnabled(boolean)
	 */
	public void setSpeechFeedbackEnabled(boolean speechFeedbackEnabled){
		if(speechFeedbackEnabled){
			mCurrentFlags |= SPEECH_FEEDBACK;
		} else {
			mCurrentFlags &= ~SPEECH_FEEDBACK;
		}
		feedbackTypeChanged = true;
	}

	/**
	 * @return Whether this feedback is enabled
	 */
	public boolean isSpeechFeedbackEnabled(){
		return SPEECH_FEEDBACK == (mCurrentFlags & SPEECH_FEEDBACK);
	}

	/**
	 * Registers an eventlistener that will be used when different events are fired.
	 * 
	 * @param mScanOrientationEventListener The listener that will be used to register events
	 */
	public void setOnScanOrientationEventListener(OnScanOrientationEventListener mScanOrientationEventListener){
		this.listener = mScanOrientationEventListener;
	}

	/**
	 * The {@link OnScanOrientationEventListener} can be unregistered and no more callbacks will
	 * be received from this module.
	 */
	public void unregisterScanOrientationEventListener(){
		this.listener = null;
	}

	private class SortedByDeviation implements Comparator<WPoint>{

		public int compare(WPoint o1, WPoint o2) {
			return Float.compare(o1.mDeviation, o2.mDeviation);
		}		
	}

	private class SortedByDistance implements Comparator<WPoint>{

		public int compare(WPoint o1, WPoint o2) {
			return Float.compare(o1.mDistance, o2.mDistance);
		}		
	}

	/**
	 * Small class representing an object that will be used by the {@link ScanOrientation}, 
	 * mainly by the list containing the points of interest.
	 */
	private class WPoint implements Comparator<WPoint>, Comparable<WPoint>{
		private float mNextDirection = Float.NaN;
		private float mDistance = Float.NaN;;
		private float mDeviation = Float.NaN;
		private WayPoint wayPoint;
		private Location wayPointLocation;

		/**
		 * @param wayPoint The {@link WayPoint} for this point of interest 
		 */
		private WPoint(WayPoint wayPoint){
			this.wayPoint = wayPoint;
			this.wayPointLocation = wayPoint.getLocation();
		}

		/**
		 * Once the GPS has been fixed, this method updates the distance and then the deviation
		 */
		private void updateValues(){
			try{
				if(mCurrentLocation != null){					
					updateDistance();
					updateDeviation();
				}
			}catch (Exception exp){
				Log.e(TAG, exp.toString());
			}
		}

		/**
		 * Refresh/updates the distance from the user's position to this point of interest.
		 */
		private void updateDistance(){			
			try{
				this.mDistance = Math.round(mCurrentLocation.distanceTo(this.wayPointLocation));
			} catch(Exception e){
				Log.e(TAG, e.toString());
			}
		}

		/**
		 * Refresh/updates the deviation to this point of interest. If the point of interest is outside
		 * the valid range due to distance, no calculations are made.
		 * 
		 * The deviation is calculated considering a full 360 degrees circle.
		 */
		private void updateDeviation(){
			if(distanceIsValid(mDistance)){
				this.mNextDirection = (360 + ScanOrientation.this.mCurrentLocation.bearingTo(this.wayPointLocation))%360;
				this.mDeviation = Math.abs(mNextDirection - mAzimuth360);
			} else {
				this.mDeviation = Float.NaN;
			}
		}

		public int compare(WPoint o1, WPoint o2) {
			double lat1, lat2, lon1, lon2;

			lat1 = o1.wayPointLocation.getLatitude();
			lat2 = o2.wayPointLocation.getLatitude();
			lon1 = o1.wayPointLocation.getLongitude();
			lon2 = o2.wayPointLocation.getLongitude();

			int ans = Double.compare(lat1, lat2);

			if(ans == 0)	{
				return Double.compare(lon1, lon2);
			} else {
				return  ans; 
			}
		}

		public int compareTo(WPoint o) {
			return compare(WPoint.this, o);
		}

		@Override
		public boolean equals(Object obj) {
			if(obj == null){
				return false;
			}
			return compareTo((WPoint)obj) == 0;
		}

		@Override
		public String toString() {
			StringBuilder builder = new StringBuilder();
			builder.append("WPoint [mNextDirection=");
			builder.append(mNextDirection);
			builder.append(", mDistance=");
			builder.append(mDistance);
			builder.append(", mDeviation=");
			builder.append(mDeviation);
			builder.append(", NAME=");
			builder.append(wayPoint.getName());
			builder.append("]");
			return builder.toString();
		}

	}

}
